﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using System.Xml.Linq;
using Ionic.Zlib;

namespace TicTacToe.Core.Statistics
{
    public partial class Database
    {
        readonly Collection<PlayerID> mPlayers;
        readonly Collection<TurnID> mTurns;
        readonly Collection<GameID> mGames;
        readonly Dictionary<GameID, PlayerID> mWinners;

        static readonly string DatabaseFilename = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "3DTicTacToe", "database.xml.gz");
        static readonly object LockObj = new object();

        XDocument CreateDatabaseDocument()
        {
            return new XDocument(
                new XDeclaration("1.0", "utf-8", null),
                new XElement("statistics",
                    new XElement("players",
                        from player in mPlayers
                        select new XElement("player-id",
                            new XElement("id", player.Id),
                            new XElement("name", player.Name))),
                    new XElement("games",
                        from game in mGames
                        select new XElement("game-id",
                            new XElement("id", game.Id),
                            new XElement("player-ref", game.Player1.Id),
                            new XElement("player-ref", game.Player2.Id),
                            new XElement("starttime", game.StartTime)),
                        from winner in mWinners
                        select new XElement("winner-id",
                            new XElement("game-ref", winner.Key.Id),
                            new XElement("player-ref", winner.Value.Id))),
                    new XElement("turns",
                        from turn in mTurns
                        select new XElement("turn-id",
                            new XElement("id", turn.Id),
                            new XElement("game-ref", turn.Game.Id),
                            new XElement("player-ref", turn.Player.Id),
                            new XElement("time", turn.Time),
                            new XElement("index", turn.Index),
                            new XElement("coordinates",
                                new XElement("x1", turn.X1),
                                new XElement("x2", turn.X2),
                                new XElement("x3", turn.X3))))));
        }

        public void SaveAsync()
        {
            Task.Factory.StartNew(Save);
        }

        // //

        internal Database()
        {
            mPlayers = new Collection<PlayerID>();
            mTurns = new Collection<TurnID>();
            mGames = new Collection<GameID>();
            mWinners = new Dictionary<GameID, PlayerID>();
        }

        internal Database(XDocument document)
            : this()
        {
            if (document.Root.Name != "statistics")
                throw new ArgumentException("Invalid XML passed to constructor");

            var root = document.Root;

            var pids = from e in root.Element("players").Elements("player-id")
                       select new PlayerID(Guid.Parse(e.Element("id").Value), e.Element("name").Value);
            mPlayers.AddRange(pids);

            var gids = from e in root.Element("games").Elements("game-id")
                       let prefs = e.Elements("player-ref").Select(n => Guid.Parse(n.Value))
                       let pid1 = GetPlayerById(prefs.First())
                       let pid2 = GetPlayerById(prefs.Last())
                       let id = Guid.Parse(e.Element("id").Value)
                       let starttime = DateTime.Parse(e.Element("starttime").Value)
                       select new GameID(id, pid1, pid2, starttime);
            mGames.AddRange(gids);

            var wids = from e in root.Element("games").Elements("winner-id")
                       let gid = GetGameById(Guid.Parse(e.Element("game-ref").Value))
                       let pid = GetPlayerById(Guid.Parse(e.Element("player-ref").Value))
                       select new KeyValuePair<GameID, PlayerID>(gid, pid);
            mWinners.AddRange(wids);

            var tids = from e in root.Element("turns").Elements("turn-id")
                       let id = Guid.Parse(e.Element("id").Value)
                       let gid = GetGameById(Guid.Parse(e.Element("game-ref").Value))
                       let pid = GetPlayerById(Guid.Parse(e.Element("player-ref").Value))
                       let time = DateTime.Parse(e.Element("time").Value)
                       let index = int.Parse(e.Element("index").Value)
                       let x1 = int.Parse(e.Element("coordinates").Element("x1").Value)
                       let x2 = int.Parse(e.Element("coordinates").Element("x2").Value)
                       let x3 = int.Parse(e.Element("coordinates").Element("x3").Value)
                       select new TurnID(id, gid, pid, time, index, x1, x2, x3);
            mTurns.AddRange(tids);
        }

        // //

        public long DatabaseSize
        {
            get
            {
                Save();
                return new FileInfo(DatabaseFilename).Length;
            }
        }

        public void Wipe()
        {
            mPlayers.Clear();
            mTurns.Clear();
            mGames.Clear();
            mWinners.Clear();
        }
        
        public void Save()
        {
            lock (LockObj)
            {
                FileMode mode;
                if (File.Exists(DatabaseFilename))
                    mode = FileMode.Truncate;
                else
                {
                    var path = Path.GetDirectoryName(DatabaseFilename);
                    mode = FileMode.CreateNew;
                    if (!Directory.Exists(path))
                        Directory.CreateDirectory(path);
                }

                var fs = new FileStream(DatabaseFilename, mode, FileAccess.Write, FileShare.None);
                using (var zipStream = new GZipStream(fs, CompressionMode.Compress, CompressionLevel.BestCompression))
                    CreateDatabaseDocument().Save(zipStream);
            }
        }

        internal static Database Load()
        {
            Database db;

            lock (LockObj)
            {
                if (File.Exists(DatabaseFilename))
                {
                    var fs = File.OpenRead(DatabaseFilename);
                    using (var zipStream = new GZipStream(fs, CompressionMode.Decompress))
                    {
                        try
                        {
                            var doc = XDocument.Load(zipStream);
                            db = new Database(doc);
                        }
                        catch (Exception)
                        {
                            db = new Database();
                        }
                    }
                }
                else
                    db = new Database();
            }

            return db;
        }
    }
}
